/*
 * Copyright (c) 2008, Roman Bosak
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  - Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *  - Neither the name of the DailyDevelopment nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.dailydev.jira.containerprovider;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Map.Entry;

import org.apache.log4j.Logger;
import org.picocontainer.PicoContainer;
import org.picocontainer.defaults.DefaultPicoContainer;

import com.atlassian.jira.extension.ContainerProvider;

/**
 * Implementation of {@link ContainerProvider} interface providing possibility
 * to replace existing Jira components by custom implementations or add new Jira
 * components to the {@link PicoContainer} (alternative to 'component' extension
 * point from standard Jira extensions).
 * 
 * <p>
 * As descibed on Jira Confluence
 * (http://confluence.atlassian.com/display/JIRA/Customising+JIRA+code) the
 * {@link ContainerProvider} implementation should be defined in
 * <code>jira-application.properties</code> file located in
 * <code>classes</code> folder of Jira application.
 * <p>
 * 
 * @author Roman Bosak, roman.bosak@gmail.com
 */
public class ConfigurableContainerProvider implements ContainerProvider {

    /**
     * Logger for this class.
     */
    private static final Logger LOG = Logger
            .getLogger(ConfigurableContainerProvider.class);

    /**
     * Classpath relative path to configuration file of this
     * {@link ConfigurableContainerProvider}.
     */
    private String configFile = "componentImplementations.properties";

    /**
     * Default constructor.
     */
    public ConfigurableContainerProvider() {
        super();
    }

    /**
     * Method returns {@link DefaultPicoContainer} instance containing
     * registrations of components as defined in file specified by
     * {@link #configFile} field value. Returned instance also wraps
     * <code>parent</code> {@link PicoContainer} so it still provides default
     * Jira component implementations registered within it not overrided by
     * child {@link PicoContainer}. In case of some IO problems with the file
     * the method returns directly <code>parent</code> instance.
     * 
     * @param parent
     *            parent instance of {@link PicoContainer} wrapped by resulting
     *            {@link DefaultPicoContainer} instance
     * @return {@link DefaultPicoContainer} instance wrapping
     *         <code>parent</code> {@link PicoContainer} and also containing
     *         registrations of components as defined in file specified by
     *         {@link #configFile} field value. In case of some IO problems with
     *         the file the method returns directly <code>parent</code>
     *         instance.
     */
    public PicoContainer getContainer(final PicoContainer parent) {
        try {
            PicoContainer container = buildContainer(parent);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Configurable container built successfully");
            }
            return container;
        } catch (final FileNotFoundException e) {
            LOG.error(
                    "Component implementations configuration file not found> "
                            + new File(configFile).getAbsolutePath(), e);
        } catch (final IOException e) {
            LOG.error("Unable to read component implementations config file> "
                    + new File(configFile).getAbsolutePath(), e);
        }

        return parent;
    }

    /**
     * Sets classpath relative path to configuration file.
     * 
     * @param configFilePath
     *            classpath relative path to configuration file
     */
    public void setConfigFile(final String configFilePath) {
        this.configFile = configFilePath;
    }

    /**
     * Method creates new {@link DefaultPicoContainer} instance wrapping
     * <code>parent</code> {@link PicoContainer} and registering component
     * implementations as defined in property file.
     * 
     * @param parent
     *            parent {@link PicoContainer} to be wrapped by newly created
     *            {@link DefaultPicoContainer} instance
     * @return new {@link DefaultPicoContainer} instance wrapping
     *         <code>parent</code> {@link PicoContainer} and containing
     *         registered component implementations as defined in property file.
     * @throws IOException
     *             thrown in case when configuration file could not be loaded
     *             for some reason
     * @throws IllegalArgumentException
     *             if the configuration file contains a malformed Unicode escape
     *             sequence.
     */
    protected PicoContainer buildContainer(final PicoContainer parent)
            throws IOException {
        // create new DefaultPicoContainer and wrap parent PicoContainer
        DefaultPicoContainer container = new DefaultPicoContainer(parent);

        // load property file and parse properties
        final Properties properties = readPropertyFile();

        // iterate through properties and register configured components
        for (final Entry<Object, Object> entry : properties.entrySet()) {
            final String interfaceClassName = (String) entry.getKey();
            final String implementationClassName = (String) entry.getValue();
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(interfaceClassName.trim());
            } catch (final ClassNotFoundException e) {
                LOG.warn("Component type class '" + interfaceClassName
                        + "' not found while registering component "
                        + "implementations => component will not be "
                        + "registered");
            }

            Class<?> implementationClass = null;
            try {
                implementationClass = Class.forName(implementationClassName
                        .trim());
            } catch (final ClassNotFoundException e) {
                LOG.warn("Component implementation '" + implementationClassName
                        + "' not found while registering component "
                        + "implementations => component will not be "
                        + "registered");
            }

            if (interfaceClass != null && implementationClass != null) {
                // register component implementation
                container.registerComponentImplementation(interfaceClass,
                        implementationClass);
            }
        }

        return container;
    }

    /**
     * Reads property file specified by path defined by {@link #configFile}
     * field.
     * 
     * @return {@link Properties} parsed from file specified as defined by
     *         {@link #configFile} field value
     * 
     * @throws IOException
     *             if the configuration file could not be loaded for some
     *             reason.
     * @throws IllegalArgumentException
     *             if the configuration file contains a malformed Unicode escape
     *             sequence.
     */
    protected Properties readPropertyFile() throws IOException {
        InputStream propertiesInputStream = ConfigurableContainerProvider.class
                .getClassLoader().getResourceAsStream(configFile);

        if (propertiesInputStream == null) {
            throw new FileNotFoundException("Configuration resource '"
                    + configFile + "' not found");
        }

        Properties properties = new Properties();
        try {
            properties.load(propertiesInputStream);
        } finally {
            try {
                propertiesInputStream.close();
            } catch (final IOException e) {
                LOG.error("Unable to close property file input stream", e);
            }
        }
        return properties;
    }
}
